package org.barad.architecture.analyzer.archpad.cell.editor;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Set;

import javax.swing.CellEditor;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.apache.log4j.Logger;
import org.barad.architecture.analyzer.archpad.component.ArchPadComponent;
import org.barad.architecture.analyzer.archpad.model.ModelPropertyDescriptorManager;
import org.barad.architecture.analyzer.archpad.model.ModelPropertyDescriptorManager.ModelPropertyDescriptor;
import org.barad.architecture.analyzer.widgets.CustomMutableTreeNode;
import org.barad.architecture.analyzer.widgets.CustomTreeCellEditor;

/**
 * This class is a component editor. It shows a GUI to the 
 * user which can be used to edit the contents of an ArchPad
 * component. 
 * 
 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
 *
 */
public class ComponentEditor extends JComponent {
	public static final long serialVersionUID = 1L;
	private Logger log;
	private JPanel modelPanel;
	private JPanel actionPanel;
	private JTree componentTree;
	private JButton okButton;
	private JButton cancelButton;
	private JButton newButton;
	private JButton deleteButton;
	private ArchPadComponent oldComponent;
	private ArchPadComponent newComponent;
	private CellEditor cellEditor;

	/**
	 * Creates a new {@link ComponentEditor} instance.
	 * 
	 * @param cellEditor The cell editor used for manipulating
	 *        a JGraph cell.
	 */
	public ComponentEditor(CellEditor cellEditor) {
		this.log = Logger.getLogger(getClass());
		this.cellEditor = cellEditor; 
		this.modelPanel = new JPanel();
		this.actionPanel = new JPanel();
		this.okButton = new JButton("OK");
		this.cancelButton = new JButton("Cancel");
		this.deleteButton = new JButton("Delete");
		this.newButton = new JButton("New");
		this.actionPanel.add(okButton);
		this.actionPanel.add(cancelButton);
		this.actionPanel.add(newButton);
		this.actionPanel.add(deleteButton);
		this.setLayout(new BorderLayout());
		this.add(new JScrollPane(modelPanel), BorderLayout.CENTER);
		this.add(actionPanel, BorderLayout.SOUTH);
		addListeners();
	}
	
	/**
	 * Adds event listeners for events in the GUI.
	 */
	private void addListeners() {
		okButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				updateComponent();
				oldComponent = newComponent; 
				cellEditor.stopCellEditing();
			}
		});
		cancelButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				cellEditor.stopCellEditing();
			}
		});
		newButton.addActionListener(new ActionListener() {
			@SuppressWarnings("unchecked")
			public void actionPerformed(ActionEvent e) {
				if (componentTree.getSelectionCount() == 0) {
					JOptionPane.showMessageDialog(null, "Please, select a node to which a new member to be added to", "Unsupported opertaion", JOptionPane.WARNING_MESSAGE);
					return;
				} 
				TreePath path = componentTree.getSelectionPath();
				CustomMutableTreeNode parent = (CustomMutableTreeNode)path.getLastPathComponent();
				Object userObject = parent.getUserObject();
				if (userObject instanceof Collection) {
					String fieldName = parent.getName() + "ContentClass";
					Class<?> clazz = newComponent.getClass();
					while (clazz != null) {
						try {
							Field userObjectClassField = clazz.getDeclaredField(fieldName);
							userObjectClassField.setAccessible(true);
							Class<?> userObjectClass = (Class<?>)userObjectClassField.get(newComponent);
							Object userObjectInstance = userObjectClass.newInstance();
							Collection collection = (Collection)userObject;
							collection.add(userObjectInstance);
							CustomMutableTreeNode child = new CustomMutableTreeNode(userObjectInstance.toString(), userObjectInstance);
							parent.add(child);
							DefaultTreeModel model = (DefaultTreeModel)componentTree.getModel();
							model.reload(parent);
							clazz = null;
						} catch (NoSuchFieldException nsfe) {
							log.debug(nsfe);
							log.debug("Trying to fing the field in a superclass");
							clazz = clazz.getSuperclass();
							if (clazz == null) {
								log.error("The setter " + fieldName + " of class " + userObject.getClass().getName() + " does not exist");
								break;
							}
						} catch (IllegalAccessException iae) {
							log.error("The field accessed for getting the class of colleection members is unaccessible", iae);
						} catch (InstantiationException ie) {
							log.error("Error while instatiating an object for adding to the collection of user objects", ie);
						}
					}
				} else {
					JOptionPane.showMessageDialog(null, "New instances could only be added to collections", "Unsupported operation", JOptionPane.WARNING_MESSAGE);
				}
			}
		});
		deleteButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (componentTree.getSelectionCount() == 0) {
					JOptionPane.showMessageDialog(null, "Please, select a node to delete first", "Unsupported opertaion", JOptionPane.WARNING_MESSAGE);
					return;
				} 
				DefaultTreeModel treeModel = (DefaultTreeModel)componentTree.getModel();	
				TreePath path = componentTree.getSelectionPath();
				CustomMutableTreeNode node = (CustomMutableTreeNode)path.getLastPathComponent();
				Object userObject = node.getUserObject();
				if (node.equals(treeModel.getRoot())) {
					JOptionPane.showMessageDialog(null, "Deleting the root node is not allowed", "Unsupported opertaion", JOptionPane.WARNING_MESSAGE);
					return;
				} else if (userObject instanceof Collection) {
					//clear user objects
					Collection<?> collection = (Collection<?>)userObject;
					collection.clear();	
					//update the tree by removing deleted nodes
					int numberOfChildern = node.getChildCount();
					for (int i = 0; i < numberOfChildern; i++) {
						MutableTreeNode child = (MutableTreeNode)node.getChildAt(0);
						treeModel.removeNodeFromParent(child);
					}
				} else {
					//node.setUserObject(null);
					CustomMutableTreeNode parent = (CustomMutableTreeNode)node.getParent();
					Object parentUserObject = parent.getUserObject();
					if (parentUserObject instanceof Collection) {
						Collection<?> collection = (Collection<?>)parentUserObject;
						treeModel.removeNodeFromParent(node);
						collection.remove(userObject);
					}
				}
				componentTree.repaint();
			}
		});
	}
	
	/**
	 * Gets the old component i.e. the component before editing.
	 *  
	 * @return The old component.
	 */
	public ArchPadComponent getComponent() {
		return oldComponent;
	}

	/**
	 * Sets a component for editing. The edits are performed
	 * on a clone of the component.
	 * 
	 * @param archPadComponent The component to be edited.
	 */
	public void setComponent(ArchPadComponent archPadComponent) {
		this.oldComponent = archPadComponent;
		this.newComponent = (ArchPadComponent)oldComponent.clone();
		buildComponentTree();
	}

	/*
	 * Build a {@link JTree} that visualizes the component. The tree
	 * allows a clean representation of the component's hierarchy.
	 */
	private void buildComponentTree() {
		if (componentTree != null) {
			modelPanel.remove(componentTree);
		}
		CustomMutableTreeNode root = new CustomMutableTreeNode(newComponent.getName(), 
				newComponent.getName());
		DefaultTreeModel model = new DefaultTreeModel(root);
		createNodes(root);
		componentTree = new JTree(model);
		componentTree.setCellEditor(new CustomTreeCellEditor(componentTree, 
				new DefaultTreeCellRenderer()));
		componentTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
		componentTree.setLocation(modelPanel.getX(), modelPanel.getY());
		Dimension dimension = new Dimension(this.getWidth() - 20, this.getHeight() - 60);
		componentTree.setSize(dimension);
		componentTree.setPreferredSize(dimension);
		componentTree.setEditable(true);
		componentTree.validate();
		modelPanel.add(componentTree);
	}

	/**
	 * Creates the tree nodes and adds them to the tree.
	 * A new node is created for each {@link ModelPropertyDescriptor}.
	 * 
	 * @param root The root node of the tree.
	 */
	private void createNodes(CustomMutableTreeNode root) {
		Class<?> clazz = newComponent.getClass();
		Set<ModelPropertyDescriptor> descriptors = 
			ModelPropertyDescriptorManager.getInstance().getModelPropertyDescriptors(clazz);
		for (ModelPropertyDescriptor descriptor: descriptors) {
			Method getter = descriptor.getGetter();
			Object value = invokeMethod(getter, newComponent, new Object[]{});
			String name = descriptor.getField().getName();
			CustomMutableTreeNode node = new CustomMutableTreeNode(name, value);
			root.add(node);
			if (value instanceof Collection) {
				Collection<?> collection = (Collection<?>)value;
				for (Object o: collection) {
					CustomMutableTreeNode child = new CustomMutableTreeNode(o.toString(), o);
					node.add(child);
				}
			}
		}
	}
	
	/*
	 * Invokes a method of an instance with given parameters via reflection.
	 * 
	 * @param method The {@link Method} to be invoked.
	 * @param owner The object owner.
	 * @param parameters The method parameters.
	 * @return The result from the method invocation.
	 */
	private Object invokeMethod(Method method, Object owner, Object[] parameters) {
		Object result = null;
		try {
			result = method.invoke(owner, parameters);
		} catch (InvocationTargetException ite) {
			log.error("Error during method invocation", ite);
		} catch (IllegalAccessException iae) {
			/*ignore - we invoke only public methods here*/
		}
		return result;
	}
	
	/*
	 * Updates a component by saving the changes to all
	 * of its children. 
	 */
	private void updateComponent() {
		CustomMutableTreeNode root = (CustomMutableTreeNode)componentTree.getModel().getRoot();
		@SuppressWarnings("unchecked")
		Enumeration<CustomMutableTreeNode> children = root.children();
		while (children.hasMoreElements()) {
			updateNode(children.nextElement());
		}
	}
	
	/*
	 * Updates a node, i.e. updates the node with the changes made 
	 * via the GUI.
	 * 
	 * @param node The node to be saved.
	 */
	private void updateNode(CustomMutableTreeNode node) {
		String nodeName = node.getName();
		String methodName = "set" + nodeName.substring(0, 1).toUpperCase() + nodeName.substring(1); 	
		Object userObject = node.getUserObject();
		Class<?> userObjectClass = userObject.getClass();
		Class<?> clazz = newComponent.getClass();
		while (clazz != null) {
			try {
				Method method = clazz.getDeclaredMethod(methodName, new Class<?>[]{userObjectClass});
				invokeMethod(method, newComponent, new Object[]{userObject});
				clazz = null;
			} catch (NoSuchMethodException nsme) {
				log.debug(nsme);
				log.debug("Trying to fing the method in a superclass");
				clazz = clazz.getSuperclass();
				if (clazz == null) {
					log.error("The setter " + methodName + " of class " + 
							userObject.getClass().getName() + " does not exist");
					break;
				}
			}
		}
	}
}
